{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6febd155",
   "metadata": {},
   "source": [
    "# 메시지 삭제\n",
    "\n",
    "그래프의 일반적인 상태 중 하나는 메시지 목록입니다. 일반적으로 해당 상태에 메시지를 추가만 합니다. 하지만 때로는 **메시지를 제거** 해야 할 수도 있습니다. \n",
    "\n",
    "이를 위해 `RemoveMessage` 수정자를 사용할 수 있습니다. 그리고, `RemoveMessage` 수정자는 `reducer` 키를 가진다는 것입니다. \n",
    "\n",
    "기본 `MessagesState`는 messages 키를 가지고 있으며, 해당 키의 reducer는 이러한 `RemoveMessage` 수정자를 허용합니다.\n",
    "\n",
    "이 reducer는 `RemoveMessage`를 사용하여 키에서 메시지를 삭제합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "64d2fa5e",
   "metadata": {},
   "source": [
    "## 설정\n",
    "\n",
    "먼저 메시지를 사용하는 간단한 그래프를 구축해보겠습니다. 필수적인 `reducer`를 포함하고 있는 `MessagesState`를 사용하고 있다는 점을 유의해주시기 바랍니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f33ce3b5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a3d5aad1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH17-LangGraph-Modules\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2cf66b1",
   "metadata": {},
   "source": [
    "## 튜토리얼 진행을 위한 기본 LangGraph 를 구축\n",
    "\n",
    "`RemoveMessage` 수정자를 사용하기 위해 필요한 기본 LangGraph 를 구축합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "e617925b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Literal\n",
    "\n",
    "from langchain_core.tools import tool\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langgraph.graph import MessagesState, StateGraph, START, END\n",
    "from langgraph.prebuilt import ToolNode, tools_condition\n",
    "\n",
    "# 체크포인트 저장을 위한 메모리 객체 초기화\n",
    "memory = MemorySaver()\n",
    "\n",
    "\n",
    "# 웹 검색 기능을 모방하는 도구 함수 정의\n",
    "@tool\n",
    "def search(query: str):\n",
    "    \"\"\"Call to surf on the web.\"\"\"\n",
    "    return \"웹 검색 결과: LangGraph 한글 튜토리얼은 https://wikidocs.net/233785 에서 확인할 수 있습니다.\"\n",
    "\n",
    "\n",
    "# 도구 목록 생성 및 도구 노드 초기화\n",
    "tools = [search]\n",
    "tool_node = ToolNode(tools)\n",
    "\n",
    "# 모델 초기화 및 도구 바인딩\n",
    "model = ChatOpenAI(model_name=\"gpt-4o-mini\")\n",
    "bound_model = model.bind_tools(tools)\n",
    "\n",
    "\n",
    "# # 대화 상태에 따른 다음 실행 노드 결정 함수\n",
    "def should_continue(state: MessagesState):\n",
    "    last_message = state[\"messages\"][-1]\n",
    "    if not last_message.tool_calls:\n",
    "        return END\n",
    "    return \"tool\"\n",
    "\n",
    "\n",
    "# LLM 모델 호출 및 응답 처리 함수\n",
    "def call_model(state: MessagesState):\n",
    "    response = model.invoke(state[\"messages\"])\n",
    "    return {\"messages\": response}\n",
    "\n",
    "\n",
    "# 상태 기반 워크플로우 그래프 초기화\n",
    "workflow = StateGraph(MessagesState)\n",
    "\n",
    "# 에이전트와 액션 노드 추가\n",
    "workflow.add_node(\"agent\", call_model)\n",
    "workflow.add_node(\"tool\", tool_node)\n",
    "\n",
    "# 시작점을 에이전트 노드로 설정\n",
    "workflow.add_edge(START, \"agent\")\n",
    "\n",
    "# 조건부 엣지 설정: 에이전트 노드 이후의 실행 흐름 정의\n",
    "workflow.add_conditional_edges(\"agent\", should_continue, {\"tool\": \"tool\", END: END})\n",
    "\n",
    "# 도구 실행 후 에이전트로 돌아가는 엣지 추가\n",
    "workflow.add_edge(\"tool\", \"agent\")\n",
    "\n",
    "# 체크포인터가 포함된 최종 실행 가능한 워크플로우 컴파일\n",
    "app = workflow.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "52fe1eb9",
   "metadata": {},
   "source": [
    "그래프를 시각화합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b7728172",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(app)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "853d9218",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import HumanMessage\n",
    "\n",
    "# 스레드 ID가 1인 기본 설정 객체 초기화\n",
    "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
    "\n",
    "# 1번째 질문 수행\n",
    "input_message = HumanMessage(\n",
    "    content=\"안녕하세요! 제 이름은 Teddy입니다. 잘 부탁드립니다.\"\n",
    ")\n",
    "\n",
    "# 스트림 모드로 메시지 처리 및 응답 출력, 마지막 메시지의 상세 정보 표시\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
    "    event[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "96952c1a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 후속 질문 수행\n",
    "input_message = HumanMessage(content=\"내 이름이 뭐라고요?\")\n",
    "\n",
    "# 스트림 모드로 두 번째 메시지 처리 및 응답 출력\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
    "    event[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7401e6b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계별 상태 확인\n",
    "messages = app.get_state(config).values[\"messages\"]\n",
    "for message in messages:\n",
    "    message.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0dbf15c5",
   "metadata": {},
   "source": [
    "## RemoveMessage 수정자를 사용하여 메시지 삭제\n",
    "\n",
    "먼저 메시지를 수동으로 삭제하는 방법을 살펴보겠습니다. 현재 스레드의 상태를 확인해보겠습니다:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "175b5942",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 앱 상태에서 메시지 목록 추출 및 저장\n",
    "messages = app.get_state(config).values[\"messages\"]\n",
    "# 메시지 목록 반환\n",
    "for message in messages:\n",
    "    message.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ddfac965",
   "metadata": {},
   "source": [
    "`update_state`를 호출하고 첫 번째 메시지의 id를 전달하면 해당 메시지가 삭제됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e94a0814",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import RemoveMessage\n",
    "\n",
    "# 메시지 배열의 첫 번째 메시지를 ID 기반으로 제거하고 앱 상태 업데이트\n",
    "app.update_state(config, {\"messages\": RemoveMessage(id=messages[0].id)})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3a7d2af2",
   "metadata": {},
   "source": [
    "이제 메시지들을 확인해보면 첫 번째 메시지가 삭제되었음을 확인할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1fc07fa0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 앱 상태에서 메시지 목록 추출 및 저장된 대화 내역 조회\n",
    "messages = app.get_state(config).values[\"messages\"]\n",
    "for message in messages:\n",
    "    message.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ae721f2",
   "metadata": {},
   "source": [
    "## 더 많은 메시지를 동적으로 삭제\n",
    "\n",
    "그래프 내부에서 프로그래밍 방식으로 메시지를 삭제할 수도 있습니다. \n",
    "\n",
    "그래프 실행이 종료될 때 오래된 메시지(3개 이전의 메시지보다 더 이전의 메시지)를 삭제하도록 그래프를 수정하는 방법을 살펴보겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "8223bbed",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import RemoveMessage\n",
    "from langgraph.graph import END\n",
    "\n",
    "\n",
    "# 메시지 개수가 3개 초과 시 오래된 메시지 삭제 및 최신 메시지만 유지\n",
    "def delete_messages(state):\n",
    "    messages = state[\"messages\"]\n",
    "    if len(messages) > 3:\n",
    "        return {\"messages\": [RemoveMessage(id=m.id) for m in messages[:-3]]}\n",
    "\n",
    "\n",
    "# 메시지 상태에 따른 다음 실행 노드 결정 로직\n",
    "def should_continue(state: MessagesState) -> Literal[\"action\", \"delete_messages\"]:\n",
    "    \"\"\"Return the next node to execute.\"\"\"\n",
    "    last_message = state[\"messages\"][-1]\n",
    "    # 함수 호출이 없는 경우 메시지 삭제 함수 실행\n",
    "    if not last_message.tool_calls:\n",
    "        return \"delete_messages\"\n",
    "    # 함수 호출이 있는 경우 액션 실행\n",
    "    return \"action\"\n",
    "\n",
    "\n",
    "# 메시지 상태 기반 워크플로우 그래프 정의\n",
    "workflow = StateGraph(MessagesState)\n",
    "\n",
    "# 에이전트와 액션 노드 추가\n",
    "workflow.add_node(\"agent\", call_model)\n",
    "workflow.add_node(\"action\", tool_node)\n",
    "\n",
    "# 메시지 삭제 노드 추가\n",
    "workflow.add_node(delete_messages)\n",
    "\n",
    "# 시작 노드에서 에이전트 노드로 연결\n",
    "workflow.add_edge(START, \"agent\")\n",
    "\n",
    "# 조건부 엣지 추가를 통한 노드 간 흐름 제어\n",
    "workflow.add_conditional_edges(\n",
    "    \"agent\",\n",
    "    should_continue,\n",
    ")\n",
    "\n",
    "# 액션 노드에서 에이전트 노드로 연결\n",
    "workflow.add_edge(\"action\", \"agent\")\n",
    "\n",
    "# 메시지 삭제 노드에서 종료 노드로 연결\n",
    "workflow.add_edge(\"delete_messages\", END)\n",
    "\n",
    "# 메모리 체크포인터를 사용하여 워크플로우 컴파일\n",
    "app = workflow.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0264921",
   "metadata": {},
   "source": [
    "그래프를 시각화합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bc4ce3af",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(app)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a83898c",
   "metadata": {},
   "source": [
    "이제 이것을 시도해볼 수 있습니다. `graph`를 두 번 호출한 다음 상태를 확인할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9bbf9bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangChain 메시지 처리를 위한 HumanMessage 클래스 임포트\n",
    "from langchain_core.messages import HumanMessage\n",
    "\n",
    "# 스레드 ID가 포함된 설정 객체 초기화\n",
    "config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
    "\n",
    "# 1번째 질문 수행\n",
    "input_message = HumanMessage(\n",
    "    content=\"안녕하세요! 제 이름은 Teddy입니다. 잘 부탁드립니다.\"\n",
    ")\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
    "    print([(message.type, message.content) for message in event[\"messages\"]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "48628a64",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 2번째 질문 수행\n",
    "input_message = HumanMessage(content=\"내 이름이 뭐라고요?\")\n",
    "\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
    "    print([(message.type, message.content) for message in event[\"messages\"]])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "83d50256",
   "metadata": {},
   "source": [
    "최종 상태를 확인해보면 메시지가 단 세 개만 있는 것을 확인할 수 있습니다. \n",
    "\n",
    "이는 이전 메시지들을 방금 삭제했기 때문입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "87b605b0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 앱 상태에서 메시지 목록 추출 및 저장\n",
    "messages = app.get_state(config).values[\"messages\"]\n",
    "# 메시지 목록 반환\n",
    "for message in messages:\n",
    "    message.pretty_print()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
